/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import Logger, {LOG_MODULE_TYPE} from 'arkanalyzer/lib/utils/logger';
import { BaseChecker, BaseMetaData } from '../BaseChecker';
import { Rule } from '../../model/Rule';
import { Defects, IssueReport } from '../../model/Defects';
import { MatcherCallback } from '../../matcher/Matchers';
import path from 'path';
import fs from 'fs';
import { Json5parser } from '../../Index';
import {ArkAssignStmt, ArkFile, ArkMethod, ArkPtrInvokeExpr, Scene, Stmt} from 'arkanalyzer';
import { StringConstant } from 'arkanalyzer/lib/core/base/Constant';

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'RedundantDependencyCheck');
const gMetaData: BaseMetaData = {
    severity: 3,
    ruleDocPath: 'docs/redundant-dependency-check.md',
    description: ''
};

export class RedundantDependencyCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public issues: IssueReport[] = [];
    private projectPath: string;
    private projectDependencies: Set<string> = new Set();
    private moduleDependencies: Map<string, Set<string>> = new Map();

    public registerMatchers(): MatcherCallback[] {
        const matchFileCb: MatcherCallback = {
            matcher: undefined,
            callback: this.check
        };
        return [matchFileCb];
    }

    public check = (scene: Scene): void => {
        scene.getFiles().forEach(arkFile => {
            this.forEachArkFile(arkFile);
        });
        if (this.projectDependencies.size > 0) {
            this.reportRedundantDeps(this.projectPath, Array.from(this.projectDependencies), 'project');
        }
        this.moduleDependencies.forEach((deps, moduleRoot) => {
            if (deps.size > 0) {
                this.reportRedundantDeps(moduleRoot, Array.from(deps), 'module');
            }
        });
    };

    private forEachArkFile(arkFile: ArkFile):void {
        this.projectPath = arkFile.getProjectDir();
        if (this.projectDependencies.size === 0) {
            const projectPkg = this.parseOhPackage(path.join(this.projectPath, 'oh-package.json5'));
            const deps = this.extractDependencies(projectPkg);
            deps.forEach(dep => this.projectDependencies.add(dep));
        }
        const moduleScene = arkFile.getModuleScene();
        const moduleRoot = moduleScene ? moduleScene.getModulePath() : null;
        if (moduleRoot && !this.moduleDependencies.has(moduleRoot)) {
            const modulePkg = this.parseOhPackage(path.join(moduleRoot, 'oh-package.json5'));
            const deps = this.extractDependencies(modulePkg);
            const depSet = new Set(deps);
            this.moduleDependencies.set(moduleRoot, depSet);
        }
        const imports = this.collectFileImports(arkFile);
        this.processDependencyUsage(imports, this.projectDependencies);
        if (moduleRoot) {
            const moduleDeps = this.moduleDependencies.get(moduleRoot);
            if (moduleDeps) {
                this.processDependencyUsage(imports, moduleDeps);
            }
        }
    }

    private processDependencyUsage(imports: string[], dependencies: Set<string>): void {
        const depArray = Array.from(dependencies).sort((a, b) => b.length - a.length);
        imports.forEach(imp => {
            if (dependencies.has(imp)) {
                dependencies.delete(imp);
                return;
            }
            const matchedDep = depArray.find(dep => dependencies.has(dep) && imp.startsWith(dep + '/'));
            if (matchedDep) {
                dependencies.delete(matchedDep);
            }
        });
    }

    private collectFileImports(file: ArkFile): string[] {
        const imports: Set<string> = new Set();
        try {
            file.getImportInfos().forEach((importInfo) => {
                if (importInfo.getFrom() && !imports.has(<string>importInfo.getFrom())) {
                    imports.add(<string>importInfo.getFrom());
                }
            });
            const dynamicImports = new Set<string>();
            const methods: ArkMethod[] = [];
            const defaultMethod = file.getDefaultClass()?.getDefaultArkMethod();
            if (defaultMethod) {
                methods.push(defaultMethod);
            }
            file.getClasses().forEach(cls => {
                methods.push(...cls.getMethods());
            });
            methods.forEach(meth => {
                const stmts = meth.getCfg()?.getStmts();
                if (stmts) {
                    this.processStatementsForImports(stmts, dynamicImports);
                }
            });
            dynamicImports.forEach(imp => {
                if (!imports.has(imp)) {
                    imports.add(imp);
                }
            });
        } catch (err) {
            logger.error(`Failed to collect imports from ${file.getFilePath()}: ${(err as Error).message}`);
        }
        return Array.from(imports);
    }

    private processStatementsForImports(stmts: Stmt[], imports: Set<string>):void {
        stmts.forEach(stmt => {
            if (!(stmt instanceof ArkAssignStmt)) { return };
            const rightOp = stmt.getRightOp();
            if (!(rightOp instanceof ArkPtrInvokeExpr)) { return };
            if (rightOp.getMethodSignature().getMethodSubSignature().getMethodName() === 'import') {
                const args = rightOp.getArgs()[0];
                if (args instanceof StringConstant) {
                    imports.add(args.getValue());
                }
            }
        });
    }

    private parseOhPackage(packagePath: string): Record<string, unknown> {
        if (!fs.existsSync(packagePath)) {
            return {};
        }
        try {
            const text = fs.readFileSync(packagePath, 'utf-8');
            return Json5parser.parseJsonText(text);
        } catch (err) {
            logger.error(`Failed to parse ${packagePath}: ${(err as Error).message}`);
            return {};
        }
    }

    private extractDependencies(data: Record<string, unknown>): string[] {
        const deps: string[] = [];
        const depSection = data.dependencies;
        if (depSection && typeof depSection === 'object') {
            for (const key of Object.keys(depSection as Record<string, unknown>)) {
                deps.push(key);
            }
        }
        return deps;
    }

    private reportRedundantDeps(basePath: string, redundantDeps: string[], level: 'project' | 'module'): void {
        const pkgPath = path.join(basePath, 'oh-package.json5');
        const message = `Unused dependencies found at ${level} level: ${redundantDeps.join(', ')}`;
        const defects = new Defects(0, 0, 0, message, this.rule.alert ?? this.metaData.severity,
            this.rule.ruleId, pkgPath, this.metaData.ruleDocPath, true, false, false);
        this.issues.push(new IssueReport(defects, undefined));
    }
}